iT邦幫忙

2026 iThome 鐵人賽

DAY 5
0
Modern Web

《NestJS 絕地求生手冊》:我用一年血淚換來的實戰排雷筆記系列 第 5

Day 5|錯覺的順序:ConfigService 為什麼注入失敗?別被 imports 陣列順序騙了

  • 分享至 

  • xImage
  •  

隨著專案開始需要透過環境變數管理資料庫設定,我們通常會結合 @nestjs/config 與 TypeORM 的 forRootAsync(),並注入 ConfigService 取得資料庫連線設定。

看似標準的設定,結果啟動時卻拋出這樣的錯誤:

Nest can't resolve dependencies of the TypeOrmModuleOptions (?).

明明 ConfigModuleTypeOrmModule 都寫在 AppModuleimports 裡,為什麼還是拿不到 ConfigService

有人第一直覺或許是:

「是不是 ConfigModule 排得太後面,TypeORM 啟動時它還沒準備好?」

但問題真的出在順序嗎?這篇就從這個直覺開始,一路找到 ConfigService 真正注入失敗的地方。

問題怎麼發生?

我們用最精簡的模組結構來重現這個問題:

@Module({
  imports: [
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({
        type: 'sqlite' as const,
        database: configService.get<string>('DB_NAME'),
        synchronize: true,
      }),
    }),
    ConfigModule.forRoot(),
  ],
})
export class AppModule {}

啟動後會拋出錯誤訊息:

[Error]: Nest can't resolve dependencies of the
TypeOrmModuleOptions (?). Please make sure that the argument ConfigService at
index [0] is available in the TypeOrmCoreModule module.

遇到這個錯誤時,先別急著把焦點放在「找不到 ConfigService」上。錯誤訊息最後其實給了最核心的提示:

ConfigServiceTypeOrmCoreModule 的模組作用域(module context)中不可用。

你可能會納悶:「我們明明沒寫過 TypeOrmCoreModule 啊?」事實上,它是 TypeOrmModule.forRootAsync() 在背後自動產生的內部模組。

這也提醒我們:當依賴注入失敗時,比起單純尋找「誰失聯了」,更重要的往往是鎖定「它是在哪個模組作用域裡失聯的」。

陷阱:是不是 ConfigModule 放太後面了?

看到這個錯誤,第一個很容易冒出來的念頭就是:

「是不是 ConfigModule 排在後面,所以 TypeORM 開始初始化時,ConfigService 還沒準備好。」

我們就直接做個實驗,把 ConfigModule.forRoot() 搬到最前面:

@Module({
  imports: [
    ConfigModule.forRoot(),        // 移到陣列第一位
    TypeOrmModule.forRootAsync({
      inject: [ConfigService],
      useFactory: (configService: ConfigService) => ({ ... }),
    }),
  ],
})

再次重啟,結果出現一樣的錯誤訊息。

根因:動態模組仍然受模組作用域限制

Nest 並不是照著 imports 陣列由上到下邊讀取邊建立模組。

在啟動時,Nest 會先從根模組開始掃描整個應用程式的模組結構,收集各個模組、動態模組,以及它們宣告的 importsprovidersexports 等資訊,建立模組之間的關係。

完成這個階段後,Nest 才會進入 provider 的建立過程,並在實例化 provider 時解析它所需要注入的依賴。

https://ithelp.ithome.com.tw/upload/images/20260919/20184306LtKNdmn8b7.jpg

這也是為什麼在這個案例中,單純交換 imports 陣列中的位置:

// 寫法 A
imports: [TypeOrmModule.forRootAsync({ ... }), ConfigModule.forRoot()]

// 寫法 B
imports: [ConfigModule.forRoot(), TypeOrmModule.forRootAsync({ ... })]

並不會改變 TypeOrmCoreModule 能不能存取 ConfigService

因為這兩種寫法都沒有改變兩個模組之間的依賴關係:ConfigModule 仍然只是被 AppModule 匯入,並沒有因此進入 TypeOrmCoreModule 可以取得依賴的模組作用域。

真正影響依賴注入的是「模組作用域」

既然不是順序問題,那真正的邊界在哪?

Nest 在解析 provider 的依賴時,會以它所在的模組作用域(Module Context)為基礎,尋找目前可見的 provider。主要包含:

  1. 該模組自己宣告的 providers
  2. 該模組透過 imports 引入,而且對方有 exports 的 provider
  3. 全域模組所匯出的 provider,例如:設定 isGlobal: trueConfigModule

換句話說,模組的可見性不會因為「共同被某個外層模組匯入」,就自動在彼此之間橫向共享。

回到我們的案例,ConfigModuleTypeOrmModule 雖然都被 AppModule 匯入,但這只代表它們都和 AppModule 建立了模組關係,並不代表 TypeOrmCoreModule 也能自動存取 ConfigModule 匯出的 ConfigService

TypeOrmModule.forRootAsync({
  inject: [ConfigService],
  useFactory: (configService: ConfigService) => ({ ... }),
})

inject: [ConfigService] 的意思只是:「等等執行 factory 時,我需要一個 ConfigService。」它不是召喚術,不會因為名字寫在這裡,Nest 就自動幫你找到 ConfigModule 並建立依賴關係。

所以真正的問題,不是 ConfigModule 還沒初始化完成,而是執行 TypeORM 設定 factory 函式的 TypeOrmCoreModule,它自己的模組作用域裡沒有可用的 ConfigService

這也解釋了為什麼錯誤訊息會特別點名 TypeOrmCoreModule——那才是真正解析 ConfigService 失敗的地方。

排雷指南

解法一:在動態模組補上 ConfigModule

既然問題是 TypeOrmCoreModule 的作用域中沒有可用的 ConfigService,那我們就把它明確補上:

TypeOrmModule.forRootAsync({
  imports: [ConfigModule],        // 加上這行,把提供 ConfigService 的模組帶入
  inject: [ConfigService],
  useFactory: (configService: ConfigService) => ({
    type: 'sqlite' as const,
    database: configService.get<string>('DB_NAME'),
    synchronize: true,
  }),
}),

這是最直接、也最符合 Nest 模組封裝設計的做法:需要在哪個模組中使用 ConfigService,就明確把提供它的 ConfigModule 引入。其實 Nest 在報錯時,就已經將這個解法寫在提示裡告訴我們了:

- If ConfigService is exported from a separate @Module, is that module imported within TypeOrmCoreModule?

解法二:將 ConfigModule 變成全域可見

既然 ConfigModule 到處都會用到,也可以從一開始就設定為全域模組(Global Module):

ConfigModule.forRoot({
  isGlobal: true, // 宣告此模組為全域
}),

設定 isGlobal: true 後,ConfigModule 就會被註冊為全域模組。之後其他模組要使用它所匯出的 provider 時,就不必逐一匯入ConfigModule

兩種解法的差異

顯式引入 ConfigModule 設定 isGlobal: true
優點 依賴關係清楚,模組之間的邊界比較明確,也較方便進行隔離測試 不必在各個模組重複宣告 imports,使用上較方便
缺點 每個需要使用的地方都必須明確引入 依賴關係較不容易直接從模組的 imports 看出來
適合情境 一般功能模組,或希望維持明確模組邊界的情況 ConfigService 這類幾乎整個專案都會使用的基礎服務

總結

  1. 依賴解析仍然受模組作用域限制forRootAsync() 的 factory 要注入什麼 provider,那個 provider 就必須在 factory 所屬的模組作用域中可見。外層 AppModule 有匯入,不代表裡面的動態模組就一定看得到。
  2. 改變模組陣列順序無助於解此 DI 問題:Nest 不是看到陣列第一個就先把它初始化完,再處理第二個。真正決定 ConfigService 能不能被注入的,仍然是模組之間的依賴關係與作用域。

參考資料


上一篇
Day 4|錯位的參數:useFactory 的 inject 陣列寫錯,為什麼傳進來的依賴全對不上?
下一篇
Day 6|型別的騙局:`ConfigService.get<number>()` 拿到的為什麼還是字串?
系列文
《NestJS 絕地求生手冊》:我用一年血淚換來的實戰排雷筆記6
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言